package asteroids.model;
import java.lang.Math;

import asteroids.IShip;
import asteroids.Util;
import be.kuleuven.cs.som.annotate.*;
import java.util.HashSet;
import asteroids.model.programs.*;


/**
 * A class representing a moving ship.
 * @invar Each ship has a valid force.
 * 			| isValidForce(force)
 * @invar Every ship has a valid radius, greater than it's static min_radius.
 * 			| isValidRadius(getRadius(), Ship.getMinRadius())
 * @invar Every bullet this Ship fires, is valid.
 * 			| for each Bullet from getBullets()
 * 			| 	isValidBullet(bullet)
 * @invar Every ship has maximum 3 'living' bullets.
 * 			| getBullets().size() <= 3
 * @invar Each ship has a valid angle.
 * 			| Util.isValidDouble(getDirection())
 * @author Martin & Anton
 *
 */

public class Ship extends SpaceObject implements IShip{
	  
	private double angle;
	private static final double min_radius = 0;
	private final double force;
	private boolean thruster;
	private HashSet<Bullet> bullets;
	private Program program;
	
	//constructors
	
	/**
	 * Creates a ship with default position (0,0), default speed (0,0), the mininum radius as radius, facing right.
	 * @effect	Ship(min_radius)
	 */
	public Ship(){
		this(min_radius);
	}
	
	/**
	 * Creates a ship with default position (0,0), default speed (0,0), the given radius as radius, facing right and mass 100.
	 * @param radius
	 * @effect	Ship(0, 0, 0, 0, radius, 0, 0, c)
	 * @throws IllegalArgumentException
	 * 			If the radius is not valid.
	 */
	public Ship(double radius) throws IllegalArgumentException{
		this(0, 0, 0, 0, radius, 0, 100, c);
	}
	
	/**
	 * Creates a ship with the given coordinates, the given speed, given radius, and given angle 
	 * 	if these are all valid specified by their respective isValid methods.
	 * @param x
	 * @param y
	 * @param speed_x
	 * @param speed_y
	 * @param radius
	 * @param angle
	 * @param mass
	 * @effect	Ship(x,y,speed_x,speed_y,radius, angle, mass, c)
	 */
	public Ship(double x, double y, double speed_x, double speed_y,  double radius, double angle, double mass) throws IllegalArgumentException{
		this(x,y,speed_x,speed_y,radius, angle, mass, c);
	}
	
	/**
	 * 
	 * @param x
	 * @param y
	 * @param speed_x
	 * @param speed_y
	 * @param radius
	 * @param angle
	 * @param mass
	 * @param max_speed
	 * @effect Ship(x,y,speed_x,speed_y,radius, mass, min_radius, max_speed, 1.1E18)
	 */
	public Ship(double x, double y, double speed_x, double speed_y,  double radius, double angle, double mass, double max_speed) throws IllegalArgumentException{
		this(x,y,speed_x,speed_y,radius, angle, mass, max_speed, 1.1E18);
	}
	
	/**
	 * Creates a ship with the given coordinates, the given speed, given radius, and given angle 
	 * 	if these are all valid specified by their respective isValid methods.
	 * @param x
	 * @param y
	 * @param speed_x
	 * @param speed_y
	 * @param radius
	 * @param angle
	 * @param mass
	 * @param max_speed
	 * @param force
	 * @post	The coordinates, speed, angle and radius will be initialized in this method.
	 * 			| (new this).radius = radius && (new this).position = new Vector(x,y)
	 * 			|	&& (new this).speed = new Speed(speed_x, speed_y) && (new this).angle = angle
	 * 			|	&& (new this).getForce() == force
	 * @throws IllegalArgumentException
	 * 			If the radius is not valid specified by isValidRadius, 
	 * 			If the mass is not valid specified by isValidMass,
	 * 			or the coordinates are not valid, specified by isValidDouble, 
	 * 			or if the speed is not valid specified by isValidSpeed.
	 * 			| !isValidRadius(radius) || !Vector.isValidDouble(x) 
	 * 			|	|| !Vector.isValidDouble(y) || !Speed.isValidSpeed(speed_x, speed_y)
	 * 			|	|| !Ship.isValidForce(force)
	 */
	public Ship(double x, double y, double speed_x, double speed_y,  double radius, double angle, double mass, double max_speed, double force) throws IllegalArgumentException{
		super(x,y,speed_x,speed_y,radius, mass, min_radius, max_speed);
		setAngle(angle);
		this.thruster = false;
		if(!Ship.isValidForce(force))
			throw new IllegalArgumentException();
		else
			this.force = force;
		bullets = new HashSet<Bullet>();
	}
	
	
	@Basic
	public double getDirection(){
		return angle;
	}

	@Basic @Immutable
	public static double getMinRadius(){
		return min_radius;
	}
	
	@Basic
	public double getForce() {
		return force;
	}
	
	@Basic
	public HashSet<Bullet> getBullets() {
		return bullets;
	}
	
	@Basic
	public Program getProgram(){
		return program;
	}
	
	@Basic
	/**
	 * 
	 * @param angle
	 * @pre		The given angle must be valid specified by isValidDouble
	 * 			| isValidDouble(angle)
	 * @post	The angle will be set as the given angle.
	 * 			| (new this).getDirection() == angle
	 */
	public void setAngle(double angle){
		assert(Util.isValidDouble(angle%(2*Math.PI)));
		this.angle =  angle%(2*Math.PI);
	}
	
	/**
	 * Set a program.
	 * @param program
	 * @post The program will be set as the given program.
	 * 			| (new this).getProgram() == program
	 * @post The program has this ship as ship.
	 * 			| (new program).getShip() == this
	 * @throws NullPointerException
	 * 			| if(program == null)
	 */
	public void setProgram(Program program) throws NullPointerException{
		if(program == null)
			throw new NullPointerException();
		this.program = program;
		program.setShip(this);
	}

	/**
	 * Set the thruster as the boolean provided.
	 * @param active
	 * @post 	If the ship is alive, the thruster will be set as the given boolean.
	 * 			| if(isAlive())
	 * 			|	(new this).getThruster() == active
	 */
	public void setThruster(boolean active) throws IllegalStateException{
		if(isAlive())
			this.thruster = active;
	}
	
	@Basic
	public boolean isThrusterActive(){
		return this.thruster;
	}

	private boolean canFireBullet(){
		if (!isAlive())
			return false;
		if(getWorld() == null)
			return false;
		if(getBullets().size() >=3)
			return false;
		else
			return true;
	}
	
	/**
	 * 
	 * @param force
	 * @return	true if it is a valid double and greater than 0
	 * 			| return == Util.isValidDouble(force) || Util.fuzzyLessThanOrEqualTo(force, 0)
	 */
	private static boolean isValidForce(double force) {
		if(!Util.isValidDouble(force))
			return false;
		if(Util.fuzzyLessThanOrEqualTo(force, 0))
			return false;
		return true;		
	}
	
	/**
	 * This method checks if the bullet can be added to his bullets.
	 * @param bullet
	 * @return
	 * 		|true if(bullet != null && !bullets.contains(bullet))
	 */
	private boolean isValidBullet(Bullet bullet) {
		if(bullet == null)
			return false;
		if(bullets.contains(bullet))
			return false;
		return true;
	}
	
	/**
	 * 
	 * @param angle
	 * @effect setAngle(angle)
	 */
	public void turn(double angle) {
		assert isAlive();
		assert Util.isValidDouble((this.getDirection() + angle)%(2*Math.PI));
		this.setAngle(this.getDirection() + angle);
	}
	
	/**
	 * This method changes the speed by accelerating to the direction the ship is facing.
	 * The magnitude of the acceleration is calculated by force/mass times time, both variables of the ship.
	 * @param 	time
	 * @effect	speed.setSpeed(getXVelocity() + Math.cos(getDirection)*getForce()/getMass()*time
	 * 			If the thruster is active.
	 * 			| if(Util.isValidDouble(time) && isThrusterActive() && time > 0 && isAlive())
	 */
	public void thrust(double time){
		if(isAlive()) {
			if(Util.isValidDouble(time) && isThrusterActive() && time > 0){
				double cos = Math.cos(getDirection()); 
				double sin = Math.sin(getDirection());
				double speedX = this.getXVelocity(); 
				double speedY = this.getYVelocity();
				double thrust = getForce()/getMass();
				if(Util.isValidDouble(thrust)) {
					if(thrust < 0) thrust = 0;
					getSpeed().setSpeed(speedX + cos*thrust*time, speedY + sin*thrust*time);
				}
			}
		}
	}
	
	/**
	 * Fire a bullet, if this ship is located in a world.
	 * @effect A new bullet is created next to the ship
	 * 			| new Bullet(getX()+cos*(getRadius()+3), getY()+sin*(getRadius()+3), cos*250, sin*250, 3, this)
	 * @throws IllegalStateException
	 * 			| if(getWorld() == null)
	 * @throws IllegalArgumentException
	 * 			| if the bullet can not spawn in the world
	 
	 */
	public void fireBullet() throws IllegalArgumentException, IllegalStateException{
		if (!canFireBullet()){
		}
		else{
			double cos = Math.cos(getDirection()); 
			double sin = Math.sin(getDirection());
			Bullet bullet = new Bullet(getX()+cos*(getRadius()+3), getY()+sin*(getRadius()+3), cos*250, sin*250, 3, this);
			try{
				getWorld().addSpaceObject(bullet);
				addBullet(bullet);
			}
			catch(IllegalArgumentException e) {}
		}
	}
	
	/**
	 * Add a bullet to this ship's bullets.
	 * @param bullet
	 * @effect bullets.add(bullet)
	 * @throws IllegalArgumentException
	 * 			| if(!isValidBullet(bullet))
	 */
	private void addBullet(Bullet bullet) throws IllegalArgumentException, IllegalStateException {
		if (!isAlive())
			throw new IllegalStateException();
		if(isValidBullet(bullet))
			bullets.add(bullet);
		else
			throw new IllegalArgumentException();
	}
	
	public void removeBullet(Bullet bullet) {
		if(bullets.contains(bullet))
			bullets.remove(bullet);
	}
	
	public void execute(double time){
		if(hasProgram())
			program.execute(time);
	}
	
	private boolean hasProgram(){
		if(program != null)
			return true;
		return false; 
		
	}
}